package com.dongni.tools;

import android.text.TextUtils;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * Logger is a wrapper of {@link Log}
 * But more pretty, simple and powerful
 *
 * @author Orhan Obut
 */
public final class Logger {

	/**
	 * Android's max limit for a log entry is ~4076 bytes,
	 * so 4000 bytes is used as chunk size since default charset
	 * is UTF-8
	 */
	private static final int CHUNK_SIZE = 4000;

	/**
	 * It is used for json pretty print
	 */
	private static final int JSON_INDENT = 4;

	/**
	 * In order to prevent readability, max method count is restricted with 5
	 */
	private static final int MAX_METHOD_COUNT = 5;

	/**
	 * It is used to determine log settings such as method count, thread info visibility
	 */
	private static final Settings settings = new Settings();

	/**
	 * Drawing toolbox
	 */
	private static final char TOP_LEFT_CORNER = '╔';
	private static final char BOTTOM_LEFT_CORNER = '╚';
	private static final char MIDDLE_CORNER = '╟';
	private static final char HORIZONTAL_DOUBLE_LINE = '║';
	private static final String DOUBLE_DIVIDER = "════════════════════════════════════════════";
	private static final String SINGLE_DIVIDER = "────────────────────────────────────────────";
	private static final String TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
	private static final String BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
	private static final String MIDDLE_BORDER = MIDDLE_CORNER + SINGLE_DIVIDER + SINGLE_DIVIDER;

	/**
	 * TAG is used for the Log, the name is a little different
	 * in order to differentiate the logs easily with the filter
	 */
	private static String TAG = "PRETTYLOGGER";

	//no instance
	private Logger() {
	}

	/**
	 * It is used to get the settings object in order to change settings
	 *
	 * @return the settings object
	 */
	public static Settings init() {
		return settings;
	}

	/**
	 * It is used to change the tag
	 *
	 * @param tag is the given string which will be used in Logger
	 */
	public static Settings init(String tag) {
		if (TextUtils.isEmpty(tag)) {
			throw new NullPointerException("tag may not be null");
		}
		Logger.TAG = tag;
		return settings;
	}

	public static void d(String message) {
		d(TAG, message);
	}

	public static void d(String tag, String message) {
		d(tag, message, settings.methodCount);
	}

	public static void d(String message, int methodCount) {
		d(TAG, message, methodCount);
	}

	public static void d(String tag, String message, int methodCount) {
		validateMethodCount(methodCount);
		log(Log.DEBUG, tag, message, methodCount);
	}

	public static void e(String message) {
		e(TAG, message);
	}

	public static void e(String tag, String message) {
		e(tag, message, null, settings.methodCount);
	}

	public static void e(Exception e) {
		e(TAG, null, e, settings.methodCount);
	}

	public static void e(String tag, Exception e) {
		e(tag, null, e, settings.methodCount);
	}

	public static void e(String message, int methodCount) {
		validateMethodCount(methodCount);
		e(message, null, methodCount);
	}

	public static void e(String tag, String message, int methodCount) {
		validateMethodCount(methodCount);
		e(tag, message, null, methodCount);
	}

	public static void e(String tag, String message, Exception e) {
		e(tag, message, e, settings.methodCount);
	}

	public static void e(String tag, String message, Exception e, int methodCount) {
		validateMethodCount(methodCount);
		if (e != null && message != null) {
			message += " : " + e.toString();
		}
		if (e != null && message == null) {
			message = e.toString();
		}
		if (message == null) {
			message = "No message/exception is set";
		}
		log(Log.ERROR, tag, message, methodCount);
	}

	public static void w(String message) {
		w(TAG, message);
	}

	public static void w(String tag, String message) {
		w(tag, message, settings.methodCount);
	}

	public static void w(String message, int methodCount) {
		w(TAG, message, methodCount);
	}

	public static void w(String tag, String message, int methodCount) {
		validateMethodCount(methodCount);
		log(Log.WARN, tag, message, methodCount);
	}

	public static void i(String message) {
		i(TAG, message);
	}

	public static void i(String tag, String message) {
		i(tag, message, settings.methodCount);
	}

	public static void i(String message, int methodCount) {
		i(TAG, message, methodCount);
	}

	public static void i(String tag, String message, int methodCount) {
		validateMethodCount(methodCount);
		log(Log.INFO, tag, message, methodCount);
	}

	public static void v(String message) {
		v(TAG, message);
	}

	public static void v(String tag, String message) {
		v(tag, message, settings.methodCount);
	}

	public static void v(String message, int methodCount) {
		v(TAG, message, methodCount);
	}

	public static void v(String tag, String message, int methodCount) {
		validateMethodCount(methodCount);
		log(Log.VERBOSE, tag, message, methodCount);
	}

	public static void wtf(String message) {
		wtf(TAG, message);
	}

	public static void wtf(String tag, String message) {
		wtf(tag, message, settings.methodCount);
	}

	public static void wtf(String message, int methodCount) {
		wtf(TAG, message, methodCount);
	}

	public static void wtf(String tag, String message, int methodCount) {
		validateMethodCount(methodCount);
		log(Log.ASSERT, tag, message, methodCount);
	}

	/**
	 * Formats the json content and print it
	 *
	 * @param json the json content
	 */
	public static void json(String json) {
		json(TAG, json);
	}

	public static void json(String tag, String json) {
		json(tag, json, settings.methodCount);
	}

	public static void json(String json, int methodCount) {
		json(TAG, json, methodCount);
	}

	/**
	 * Formats the json content and print it
	 *
	 * @param json        the json content
	 * @param methodCount number of the method that will be printed
	 */
	public static void json(String tag, String json, int methodCount) {
		validateMethodCount(methodCount);
		if (TextUtils.isEmpty(json)) {
			d(tag, "Empty/Null json content", methodCount);
			return;
		}
		try {
			if (json.startsWith("{")) {
				JSONObject jsonObject = new JSONObject(json);
				String message = jsonObject.toString(JSON_INDENT);
				d(tag, message, methodCount);
				return;
			}
			if (json.startsWith("[")) {
				JSONArray jsonArray = new JSONArray(json);
				String message = jsonArray.toString(JSON_INDENT);
				d(tag, message, methodCount);
			}
		} catch (JSONException e) {
			d(tag, e.getCause().getMessage() + "\n" + json, methodCount);
		}
	}

	private static void log(int logType, String tag, String message, int methodCount) {
		if (settings.logLevel == LogLevel.NONE) {
			return;
		}
		logTopBorder(logType, tag);
		logHeaderContent(logType, tag, methodCount);

		//get bytes of message with system's default charset (which is UTF-8 for Android)
		byte[] bytes = message.getBytes();
		int length = bytes.length;
		if (length <= CHUNK_SIZE) {
			if (methodCount > 0) {
				logDivider(logType, tag);
			}
			logContent(logType, tag, message);
			logBottomBorder(logType, tag);
			return;
		}
		if (methodCount > 0) {
			logDivider(logType, tag);
		}
		for (int i = 0; i < length; i += CHUNK_SIZE) {
			int count = Math.min(length - i, CHUNK_SIZE);
			//create a new String with system's default charset (which is UTF-8 for Android)
			logContent(logType, tag, new String(bytes, i, count));
		}
		logBottomBorder(logType, tag);
	}

	private static void logTopBorder(int logType, String tag) {
		logChunk(logType, tag, TOP_BORDER);
	}

	private static void logHeaderContent(int logType, String tag, int methodCount) {
		StackTraceElement[] trace = Thread.currentThread().getStackTrace();
		if (settings.showThreadInfo) {
			logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + " Thread: " + Thread.currentThread().getName());
			logDivider(logType, tag);
		}
		String level = "";
		for (int i = methodCount; i > 0; i--) {
			int stackIndex = i + 5;
			StringBuilder builder = new StringBuilder();
			builder.append("║ ")
					.append(level)
					.append(getSimpleClassName(trace[stackIndex].getClassName()))
					.append(".")
					.append(trace[stackIndex].getMethodName())
					.append(" ")
					.append(" (")
					.append(trace[stackIndex].getFileName())
					.append(":")
					.append(trace[stackIndex].getLineNumber())
					.append(")");
			level += "   ";
			logChunk(logType, tag, builder.toString());
		}
	}

	private static void logBottomBorder(int logType, String tag) {
		logChunk(logType, tag, BOTTOM_BORDER);
	}

	private static void logDivider(int logType, String tag) {
		logChunk(logType, tag, MIDDLE_BORDER);
	}

	private static void logContent(int logType, String tag, String chunk) {
		String[] lines = chunk.split(System.getProperty("line.separator"));
		for (String line : lines) {
			logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + " " + line);
		}
	}

	private static void logChunk(int logType, String tag, String chunk) {
		String finalTag = formatTag(tag);
		switch (logType) {
			case Log.ERROR:
				Log.e(finalTag, chunk);
				break;
			case Log.INFO:
				Log.i(finalTag, chunk);
				break;
			case Log.VERBOSE:
				Log.v(finalTag, chunk);
				break;
			case Log.WARN:
				Log.w(finalTag, chunk);
				break;
			case Log.ASSERT:
				Log.wtf(finalTag, chunk);
				break;
			case Log.DEBUG:
				// Fall through, log debug by default
			default:
				Log.d(finalTag, chunk);
				break;
		}
	}

	private static String getSimpleClassName(String name) {
		int lastIndex = name.lastIndexOf(".");
		return name.substring(lastIndex + 1);
	}

	private static void validateMethodCount(int methodCount) {
		if (methodCount < 0 || methodCount > MAX_METHOD_COUNT) {
			throw new IllegalStateException("methodCount must be > 0 and < 5");
		}
	}

	private static String formatTag(String tag) {
		if (!TextUtils.isEmpty(tag) && !TextUtils.equals(TAG, tag)) {
			return TAG + "-" + tag;
		}
		return TAG;
	}

	public static class Settings {
		int methodCount = 2;
		boolean showThreadInfo = true;

		/**
		 * Determines how logs will printed
		 */
		LogLevel logLevel = LogLevel.FULL;

		public Settings hideThreadInfo() {
			showThreadInfo = false;
			return this;
		}

		public Settings setMethodCount(int methodCount) {
			validateMethodCount(methodCount);
			this.methodCount = methodCount;
			return this;
		}

		public Settings setLogLevel(LogLevel logLevel) {
			this.logLevel = logLevel;
			return this;
		}
	}

	public enum LogLevel {
		FULL, NONE
	}
}
